/// <reference path="../define/ProtobufHelper.d.ts" />

import protobufjs from 'protobufjs/minimal.js';


/**
 * protobuf的工具类,实例化使用
 * 建立在数据传输协议基础上的功能封装,如消息类型码映射成proto里的具体类型等
 */
export class ProtobufHelper {
    private anyTypeUrl = "type.googleapis.com/";
    private util: any;
    private pRoot: any;
    private lvNS: any;
    private opcodeToName: (op: number) => string;

    /**
     * Creates an instance of ProtobufHelper.
     * @param {any} root 没注册到全局则需要自行获取传入; 全局则传null,会自动取$protobufjs.roots["default"]
     * @param {string} msgPackageName proto里的包名
     * @param {(op: number) => string} opcodeToName 将类型码映射为类名
     * @param {(any | null)} [typeRegGlobal=globalThis] msgPackageName将被注册到这个对象下,方便枚举可以直接无缝使用,不传使用globalThis(web环境用window,node环境用global),传null则不注册
     * @memberof ProtobufHelper
     */
    constructor(root: any, msgPackageName: string, opcodeToName: (op: number) => string, typeRegGlobal: any | null = globalThis) {
        this.util = protobufjs.util;
        this.pRoot = root || protobufjs.roots["default"];
        this.opcodeToName = opcodeToName;
        this.lvNS = this.pRoot;
        var winReg = typeRegGlobal;
        var ns = msgPackageName.split('.');
        for (var i = 0; i < ns.length; i++) {
            this.lvNS = this.lvNS[ns[i]];
            if (winReg) {
                if (i < ns.length - 1) {
                    //非最后一级,则创建命名空间
                    winReg = winReg[ns[i]] = {};
                } else {
                    //最后一级命名空间了
                    winReg = winReg[ns[i]] = this.lvNS;
                }
            }
            if (!this.lvNS) {
                break;
            }
        }
        if (!this.lvNS) throw ('protobuf.roots["default"]."+msgPackageName+"不存在,js生成异常或者未引用');

    }

    private getMsgProto(msgName: string): any {
        var msgProto = this.lvNS[msgName];
        if (!msgProto) {
            console.error('未知的消息类型:' + msgName + ',可能是protobufjs未同步生成');
            return null;
        }
        return msgProto;
    }


    /**
     *根据类型码获取消息类名
     *
     * @param {number} op
     * @return {*}  {(string | null)}
     * @memberof ProtobufHelper
     */
    public getMsgName(op: number): string | null {
        var msgName = this.opcodeToName(op);
        if (!msgName) {
            console.error('未知的类型码:' + op);
            return null;
        }
        return msgName;
    }

    /**
     *根据消息类名获取类型
     *
     * @template T
     * @param {string} className
     * @return {*}  {(T | null)}
     * @memberof ProtobufHelper
     */
    public getClass<T>(className: string): T | null {
        var msgProto = this.lvNS[className];
        if (!msgProto) {
            console.error('未知的消息类型:' + className + ',可能是protobufjs未同步生成');
            return null;
        }
        return msgProto;
    }

    /**
     *将类型码和消息体编码成二进制数组
     *
     * @param {number} op
     * @param {object} msg
     * @return {*}  {(Uint8Array|null)}
     * @memberof ProtobufHelper
     */
    public encode(op: number, msg: any): Uint8Array | null {
        var msgName = this.getMsgName(op);
        if (!msgName) return null;
        var msgProto = this.getMsgProto(msgName);
        if (!msgProto) return null;

        return msgProto.encode(msg).finish();
    }
    /**
     *根据类型码和消息二进制,解码成消息对象
     *
     * @param {number} op
     * @param {Uint8Array} buf
     * @return {*}  {(object| null)}
     * @memberof ProtobufHelper
     */
    public decode(op: number, buf: Uint8Array): any | null {
        var msgName = this.getMsgName(op);
        if (!msgName) return null;
        var msgProto = this.getMsgProto(msgName);
        if (!msgProto) return null;

        return msgProto.decode(buf);
    }

    /**
     *将any类型解包出消息对象,解析失败返回null (root中必须包含google.protobuf.Any对象,即导入官方的Any.proto文件)
     *
     * @param {*} anyObj
     * @return {*}  {(object | null)}
     * @memberof ProtobufHelper
     */
    public anyUnpack(anyObj: google.protobuf.IAny): AnyUnpackResult | null {

        var any = new this.pRoot.google.protobuf.Any();
        if (anyObj.typeUrl != null) {
            any.typeUrl = String(anyObj.typeUrl);
        }
        if (anyObj.value != null) {
            if (typeof (anyObj.value) === "string") {
                this.util.base64.decode(anyObj.value, any.value = this.util.newBuffer(this.util.base64.length(anyObj.value)), 0);
            }
            else if (anyObj.value.length) {
                any.value = anyObj.value;
            }
        }

        if (any.typeUrl.indexOf(this.anyTypeUrl) != 0) {
            console.error('any[' + any.typeUrl + ']解析失败');
            return null;
        }
        var typeStr = any.typeUrl.substr(this.anyTypeUrl.length);
        var typeArr = typeStr.split('.');
        var type = this.pRoot;
        for (var i = 0; i < typeArr.length; i++) {
            if (!type) {
                console.error('any[' + any.typeUrl + ']解析失败');
                return null;
            }
            type = type[typeArr[i]];
        }
        return {
            unpackObject: type.decode(any.value),
            typeName: typeStr
        };
    }

    /**
     *将消息对象打包成google.protobuf.Any类型 (root中必须包含google.protobuf.Any对象,即导入官方的Any.proto文件)
     *
     * @param {*} anyObj
     * @param {string} typeFullName
     * @return {google.protobuf.Any} 
     * @memberof ProtobufHelper
     */
    public anyPack(anyObj: any, typeFullName: string): google.protobuf.IAny | null {

        var typeArr = typeFullName.split('.');
        var type = this.pRoot;
        for (var i = 0; i < typeArr.length; i++) {
            if (!type) {
                console.error('[' + typeFullName + ']找不到打包器');
                return null;
            }
            type = type[typeArr[i]];
        }

        var objBuffer = type.encode(anyObj).finish();
        var objBase64 = this.util.base64.encode(objBuffer, 0, objBuffer.length);
        var any = new this.pRoot.google.protobuf.Any() as google.protobuf.IAny;
        any.typeUrl = this.anyTypeUrl + typeFullName;
        any.value = objBase64;
        return any;
    }

}

